Chapter 4: Functions Part 1
From book Python Programming (Problem solving, Packages and Libraries) published by McGraw Hill Education (India) Private limited. By:

  • Anurag Gupta
  • G. P. Biswas

Note the following:-

  1. This html document is meant as an accompaniment to Chapter 4 Functions Part 1 .
  2. The document contains scripts executed on IDLE as well as on Jupyter notebook.
  3. The scripts executed on Jupyter can be directly copied and run into a Jupyter notebook or some other IDE (Like Pycharm or Eclipse with PyDev or Visual studio).
  4. However the scripts on IDLE also contain the >>> symbol and therefore cannot be directly executed. If you want to execute them on IDLE or Jupyter, you need to manually remove the >>> symbol.
  5. Wherever needed some background material from the book is also included to help you better understand the scripts
  6. The topic numbers given on each paragraph match the topic numbers of the book, so you can easily identify the topics and corresponding scripts.
  7. In some of the scripts, the file paths give are that of the author's computer. You need to replace them with file paths of your own computer.
  8. At some places, to improve readability, page numbers of the book are indicated in green font like:- See Page 181 of the book
  9. This document was first created as a Jupyter Notebook as combination of Markdown and code cells (extension .ipynb) and then downloaded as html. If someone wants to "modify" or "extend' this document, you may ask for the original .ipynb file by sending me an e-mail at:- 999.anuraggupta@gmail.com

4.3.3 Calling a function (As opposed to defining or writing a function)

# ---ON IDLE--- 
>>>import os
>>> myCwd = os.getcwd()        # Function which takes no arguments
>>> myCwd
'C:\\Python34'
>>> int(9.1234)      # Function casts float into int. Takes 1 argument
9
>>> curPath = os.chdir('C:\\')  #Function which returns None always
>>>print(curPath)
None
>>> os.getcwd()
'C:\\'

4.3.4 Some important built-in functions in Python
Python has a number of “built-in” functions. Some important ones are as follows:
(i) abs(x)

This function:

  • Returns the absolute value of a number.
  • The argument to this function may be an integer or a floating point number.
  • If a complex number is given as argument, then the function returns its magnitude.

The following examples clarify the concepts:

# ---ON IDLE---   
>>> abs(-9) 
9
>>> abs(3 + 4j) # abs is under root of 3 square plus 4 square
5.0
>>> abs(-3 -4j) # abs is under root of -3 square and -4 square
5.0

(ii) bool([x])
The use of square brackets indicates that the parameter is optional. If you don’t give a parameter to the bool() function, it will return a False.
This function does the following:

  • It “converts” a value to a Boolean equivalent;
  • In the “conversion”, it uses the standard truth testing procedure;
  • If x is either False or is not given, then a value of False is returned; otherwise a value of True is returned.

In Python, the following values are considered False:

  • Zero (Zero may be 0 or 0.0 or 0j). <\li>

  • None (A None evaluates to bool False) <\li>

  • False (A False obviously evaluates to a bool False). <\li>

  • All empty sequences. (Empty lists, such as [], empty tuples, such as () etc. all evaluate to False. But a list containing any element, even a zero, will not evaluate to bool False. <\li>

  • An empty mapping like an empty dictionary, that is, {} will also evaluate to bool False. <\li>

  • Note that anything except what is mentioned above will evaluate to bool True. So an object of any type always evaluates to bool True. <\li> </ul> </div>
    The following on IDLE shows how bool() works:-

# ---ON IDLE---
>>> bool()  # bool() ie without an argument is False
False
>>> bool(None) #bool() with None is False
False
>>> bool(False) # bool() of False is False
False
>>> bool(0)     #bool() of 0 is False
False
>>> bool([])    # bool() of an empty list is False
False
>>> bool(())    #bool() of an empty tuple is False
False
>>> bool(-1)    # bool() of a non-zero number even if negative is True
True

(iv) cmp(x, y)
The cmp(x,y) function does the following:

  • Compares two objects x and y and returns an integer according to the outcome.
  • If $x < y$, the return is -1.
  • If $x == y$, return is 0.
  • If $x > y$, the return is +1.
NOTE:- The cmp() method has been deprecated in Python 3.x. So, instead of using cmp(x,y) it is better to use ((x > y) – (x < y). Note that x > y when joined to x < y with a minus sign ie ‘-‘ then they will be implicitly cast into ints. So this is equivalent to (int(x > y) – int(x < y)). Further note that bool value True will cast into int 1 and bool value False will cast into int 0.

Example code:-

# ---ON IDLE---   
>>>True + True # If a plus between two True, they are cast to int
2
>>>True + False  # False is cast into int 0
1
>>>False + False
0

So if x is greater than y then $((x >y) – (x < y))$ will become 1, if x is equal to y, it will be 0 and if x is less than y, it will be -1.

# ---ON IDLE---   
>>> (5>3) - (5<3)   #Implicit cast of bool to int
1
>>> (int(5>3) - int(5<3))   #Explicit cast of bool to int
1
>>> (3>5) - (3<5)
-1
>>> (5 ==5) - (5 == 5)
0

(v) divmod(x,y)
The divmod(x, y) function does the following:

  • Takes as arguments two numbers x and y.
  • It returns a tuple of numbers (q, r), where q is the quotient and r is the remainder.
  • If the two arguments x and y are integers, then the result is the same as (x//y, x% y).
  • If either of x or y are floats, then q is the whole part of the quotient and r is x –(q*y).
  • If $y =0$, you get Zero Division Error.
  • If $x = 0$, you get (0, 0) Example code:
# ---ON IDLE---   
>>> divmod(29,5)    # 29 is dividend and 5 is divisor
(5, 4)
>>> divmod(1.5, 0.9)    # works for floats also
(1.0, 0.6)

(vi) float(x)
The important points regarding this function are as follows:

  • The function casts a variable to a floating point number.
  • The function takes as parameters either an integer, long or string. If the input is a string, it must contain only digits with or without a decimal point and with or without a sign, that is, ‘+’ or ‘-‘. The parameter given to the function can be exponential form also, such as 1.0e6 or 1.0E6.
  • The return type of the function is a float. This will be clear from following examples:
# ---ON IDLE---   
>>> float(2)    # Convert an int to float
2.0
>>> float('-345.6') #Since string has only digits, sign ie '-' and decimal so OK
-345.6
>>> float(2e3)  #Can convert number in exponential form
2000.0
>>> float(-3E-3)# Exponential form can be with 'e' or 'E' and with '-' sign also
-0.003
>>> float('abc')    #String 'abc' cannot be converted to float -> error
Traceback (most recent call last):
  ...rest of error message.....
ValueError: could not convert string to float: 'abc'

(vii) id(object)
The id(object) function does the following:

  • Gives the “id” of an object.
  • The “id” of an integer is unique and constant for this object.
  • You can think of the number returned by the id() function as a unique number given to each object.

Example code:

# ---ON IDLE---   
>>> myS = 'abc'
>>> id(myS) # String objects like all objects have id
4690496
>>> id(2)   # Even integers (like 2) have id
1474150432

(viii) int(x)
The important features of this function are as follows:

  • It casts a variable to an integer.
  • It takes 1 parameter (which may be long, string or float).
  • The return type is an integer.
  • The function int(x) converts a number or string x to an integer. If no argument is given to the int() function, it returns 0.
  • If x cannot be converted to an integer, then an error will be thrown.

This is clear from the following example:

# ---ON IDLE---   
>>> x = int()   #If no argument to int() returns a 0
>>> x
0
>>> myNum = 3.2# Lets take a float
>>> myInt = int(myNum)  #Use int to create ie return the int of myNum
>>> myInt               #myInt is an integer
3
>>> myNum               # But myNum continues to be float
3.2
>>> mySt = '123'# Take a string
>>> myInt2 = int(mySt)  # Again int() returns string equivalent of mySt
>>> mySt                # mySt continues to be a string
'123'
>>> myInt2              # But myInt2 is an integer
123
>>> int('45L')  #Will throw error as string '45L' not convertible to int
... rest of error ...
ValueError: invalid literal for int() with base 10: '45L'
>>> int('12.3') # However you cannot convert to int a string with decimal
... rest of error ...
ValueError: invalid literal for int() with base 10: '12.3'

(ix) len(x)
The len(x) function does the following:

  • Returns the length of the given object x.
  • The argument x must either be a sequence (such as a string, range, list or tuple.) or a collection (such as a set or a dictionary.)
  • If the argument s is a string, then len(s) will return the length of the string s.
  • If the argument s is either a sequence or a container, then len(s) gives the number of “items” in the sequence/ container.

Example code:

# ---ON IDLE---   
>>> len(['a', 'b', 'c'])    # 3 items in list
3
>>> len('123456789')    # 9 characters in the string
9

(x) max(s), or max(arg1, arg2, arg3, .... argN) function
There are two variations of the max function:
1. max(s)` where s is a non-empty iterable object, such as a string, list, tuple, and so on.

This will be clear from the following:-

# ---ON IDLE---   
>>> max('abcdefg')  # you can give a string to max() because string is iterable
'g'
>>> max([1,2,3,4])  #List is also iterable
4
>>> max([])     #Empty list will give error
Traceback (most recent call last):
  File "<pyshell#66>", line 1, in<module>
    max([])
ValueError: max() arg is an empty sequence

2. max(arg1, arg2, arg3,...argN)
Here arg1, arg2, arg3,... argN are the arguments given and then the max() function returns the largest of these given arguments. Note that if the arguments are strings, then the max function will return the string beginning with the character with the largest Unicode.
his is shown as follows:-

# ---ON IDLE---   
>>> max(22,33,99,44,55,66,00)
99
>>> max('a', 'A')   #Unicode of 'a' is more than 'A'
'a'

(xi) min(s), or min(arg1, arg2, arg3, .... argN) function
Just like the max() function, there are two variations of the min() function:

  1. min(s) where s is a non-empty iterable object like a string, list, tuple etc:-
# ---ON IDLE---   
>>> min('aAbBcC')
‘A’

2. min(arg1, arg2, arg3,...argN)
Here arg1, arg2, arg3,... argN are the arguments given and then the max() function returns the smallest of these given arguments.

# ---ON IDLE---   
>>> min([2,8,1,0,-3,100])
-3

(xii) range(start, stop[, step])
Some important points to note about the range(start, stop[, step]) are as follows:

  • There are three versions of this function:-
    • **`range(stop)`**
    • **`range(start, stop)`**
    • **`range(start, stop[, step])`**
  • This function is discussed in detail in the topic on flow control. However, for the present, only one form of the function, that is, range(n) or range(stop) is discussed. Moreover, it is presumed that n is a positive integer. (Other forms of this function with negative integer etc also exist but are discussed later).
  • range(n) will generate a sequence of numbers from 0 to n-1.
  • Here again, there is a difference between Python 2.x and Python 3.x. In Python 2.x on IDLE if you input range(5), output is a list, that is, [0, 1, 2, 3, 4]. But in Python 3.x, the output is not a list. Rather, it is a “range object” which is iterable, that is, which can be moved over one by one and can also be converted into other Python objects, such as a list.

This is shown as follows:-

# ---ON IDLE---   
>>> range(5) # In Python 2.x
[0,1,2,3,4]
>>> range(5) # In Python 3.x
range(0, 5)
>>> list(range(5)) #In 3.x if you want a list, you need to use list() function
[0, 1, 2, 3, 4]

(xiii) round(number[, ndigits])
The function round(number [, ndigits]) does the following:-

  • Takes two parameters and returns the “rounded off value” of the given “number”. The first parameter is “number” and denotes the number, which is to be “rounded off”. The second parameter “ndigits” is optional and denotes the number of digits to which the rounding-off is to be done.
  • If ndigits is omitted, it defaults to zero. (Again, note that square brackets indicates parameters with default values, and hence, are optional. Here, n digits is in square brackets and it has a default value of 0. So if no value of ndigits is given, it defaults to 0.)

This is clear from the following example:

# ---ON IDLE---   
>>> round(2.345)# Second parameter not given so rounded to 0 digits after decimal
2
>>>round(2.345,1) # Second parameter is 1. So rounded to 1 digit after decimal
2.3

(xiv) str(object='')
The function str(object= '') does the following:

  • Returns a string version of object.
  • If an object does not provide “its own string version” then the function str(object), returns the empty string.

You can think of the str() function in two different ways:

  • First, you can think of it as a “casting” function which creates a cast of the object into a string. (Note that the original object remains but a string cast of the object is returned). For example, you can “cast” or create strings out of other data types or objects, such as integers or floats. Of course if you don’t give any parameter to the str() function then an “empty” string is created.

This is shown as follows:

# ---ON IDLE---   
>>> str(1.2345) # a float object is cast into a string
'1.2345'
>>> str(1==1) #Outcome of 1 ==1 is True which is cast to string
'True'
>>> str()   # No parameter to str() so an empty string created
''
  • A second way of thinking of the str() function is that it creates a “string representation” of an object. This topic is covered later in the book.

(xv) tuple([iterable])
The function tuple([iterable]) does the following:

  • Takes an “iterable” as an argument. For the current context, think of an iterable as a sequence or a container.
  • The function tuple([iterable]), converts the iterable into a tuple and returns it. Remember that a tuple is a sequence so the “order of items” is important. So when an iterable is converted into a tuple by the tuple([iterable]) function, the order of items in the tuple is the same as in the iterable.
  • If the iterable given is already a tuple, then it is returned unchanged.
  • Hence, you can give a string, a list or even another tuple to the tuple function.
  • If no argument is given, an empty tuple is created.

Example code:-

# ---ON IDLE---   
>>> tuple('abcd')   #Since string is a sequence so convertible to tuple using tuple()
('a', 'b', 'c', 'd')
>>> tuple([1,2,3]) #List also convertible to tuple using tuple()
(1, 2, 3)
>>> tuple(('a', 'b', 'c')) # A tuple can be given as argument to tuple()
('a', 'b', 'c')
>>> tuple() #If no parameter, empty tuple created
()

(xvi) any(iterable) function
The any() function takes an iterable as its argument. The concept of an iterable is explained later. For the present, you can think of an iterable as a container, such as a string, list, tuple, and so on. If any item in the iterable evaluates to True, then the function returns True.

In [1]:
myL = [1, 2, 3, 4]
print(any(myL)) # gives True
myS = ''  # Empty string
print(any(myS)) # gives False
myT = ()  # Empty tuple
print(any(myT))  # gives False
True
False
False

4.3.5 Some important functions in modules in Python
The following script shows use of some important functions of math module:- (Shown here is use of functions ceil(), fabs() and floor())

# ---ON IDLE---   
>>>import math
>>> math.ceil(7.001) 
8
>>> math.fabs(-12)
12.0
>>> math.floor(-7.99)   # floor() of -7.99 is -8 and not -7 (-7 would be truncate)
-8
>>> math.ceil(-7.99)    #ceil() of -7.99 will be -7 not -8
-7

math.exp(x) function
The function exp(x) returns $e^x$.
Following script shows this:-

# ---ON IDLE---   
>>>math.exp(1) # This will give value of e. Since e**1 -> e
2.718281828459045
>>> math.exp(2) # This will give value of e ** 2
7.38905609893065

math.log(x[, base])
The function math.log(x[, base]) does the following:

  • Takes two arguments, that is, “x” and “base”. Out of these, the second argument is optional and has a default value “e”, that is, the natural log. So if no “base” is given, it is presumed to be “e”.
  • It returns the log of the number “x”. With one argument, the function, returns the natural logarithm of x (to base e). With two arguments, the function returns the logarithm of x to the given base. Note that the math module has the mathematical constant e, which is available as math.e and its value is 2.718281828459045.
# ---ON IDLE---   
>>> math.log(math.e) # math.e will give value of e whose natural log is 1
1.0
>>> math.log(7.39)# Also 7.39 little larger than e ** 2 so log(7.39) approx 2
2.0001277349601105
>>> math.log(100) # e ** 4.60 is approx 100
4.605170185988092
>>> math.log(100,10) # 10 ** 2 -> 100
2.0

math.pow(x, y)
The method pow(x, y) does the following:

  • Takes two arguments x and y, where x is the “base” and y is the “power” to which it is raised.
  • The method gives x raised to power y. So, this method is similar to $x ^ y$. However, unlike the built-in ** operator, math.pow() converts both its arguments to type float.
  • However, if x is negative, then y must be an integer. If x is negative and y is not an integer, then a “ValueError” will be raised.
  • Note, pow(x,y) is also a built-in function in Python so you can use pow(x,y) directly, that is, without using math.pow(x,y) also. It is also available as x**y. You should use ** or the built-in pow() function for computing exact integer powers (that is, when both x and y are integers).
    Example code:
# ---ON IDLE---   
>>>pow(2,2) #Inbuilt pow(x,y) function. For integers returns int
4
>>>math.pow(2,2) # pow(x,y) function of math module. Returns float
4.0
>>>2**2# Inbuilt exponential operator in python. Returns int for integers
4

math.degrees(x) This method converts the given angle x from radians to degrees.
math.radians(x)
This method converts the given angle x from degrees to radians. Note that the math module has a mathematical constant pi. This can be accessed as math.pi and is 3.141592653589793.

# ---ON IDLE---   
>>> math.sin(math.pi/2) # sin(π/2) is 1.0 and it is float
1.0
>>> math.cos(math.pi/3) # cos(π/3) is 0.5 and float
0.5000000000000001
>>>math.tan(math.pi/4) # tan(π/4) is 1.0 which here is 0.9999...
0.9999999999999999

random.random()
The following points regarding random.random() are important:

  • This is the basic method of the random module.
  • It generates a random number in semi-open range [0.0, 1.0).
  • In the range [0.0, 1), there is a “square bracket” to the left and a “round paranthesis” to the right. The square bracket indicates a “closed interval to the left” and the round parenthesis indicates an “open interval to the right”. In other words, 0.0 is possible, 1.0 is not possible. You can say that the number generated is $0 ≤ x < 1$. Semiopen range here means that the lower limit, that is, 0.0 is included but the upper limit, that is, 1.0 is excluded.
  • Suppose you want a bigger random number, you can multiply the return value with an appropriate number. The following examples clarify the concept:
# ---ON IDLE---   
>>>import random
>>> random.random() #Generates random number in range [0,1)
0.1453192289647084
>>> int(random.random() * 100) #If you want random int in [0, 100)ie 0 to 99
73

random.seed([a = None])
The following points regarding the method random.seed([a = None]) are noteworthy:

  • The method takes an optional parameter “a” with a default value of None. So you may omit “a”, or if you give a seed of None, then by default, the method uses the current system time as seed value.
  • You can give an object as value to the optional parameter “a”. However, if you choose to give some object as parameter to the seed() method, then this object must be a “hashable” object.
  • Note, a hashable object can be thought of as an immutable object. So anything, which is immutable can be used as an argument to random([a]), where “a” is an immutable object. Since integers, floats, strings, and so on are immutable, they can be used as arguments to random.seed() function.
    Examples:-
# ---ON IDLE---   
>>> random.seed(25) # can seed with an int
>>> random.random()
0.376962302390386
>>> random.random()
0.9267885077263207
>>> random.seed(25) # If you seed with same int, the series is repeated
>>> random.random()
0.376962302390386
>>> random.random()
0.9267885077263207

random.choice(sequence)
Here, sequence can be any sequence. So sequence could be a string, list or tuple. This method returns an item randomly selected from the given sequence.

# ---ON IDLE---   
>>> random.choice('abcdefghijklmnopqrstuvwxyz') # A string is a sequence
'p'
>>> random.choice(range(1000)) # Return of range() function is also a sequence
43
>>> random.choice([1,2,3,4,5,6,7,8,9]) # List is a sequence
5

random.uniform(ax, y)
This method gives a random floating point number f such that $x ≤ f ≤ y$ for $x ≤ y$. If $x ≥ y$, then you get $x ≥ f ≥ y$.
Some examples are as follows:

# ---ON IDLE---   
>>> random.uniform(1, 100) # For x <= N <= y
68.98105318225107
>>> random.uniform(100,1) # For y <= N <= x
17.608188185718944
>>> random.uniform(1,1) # For x = y, always x (or y)
1.0

randrange([start], stop, [step])
The following regarding random.randrange([start], stop, [step]) function are noteworthy:

  • The function takes three parameters out of which two, namely “start” and “stop” are optional and only one, that is, “stop” is mandatory.
  • The “start” parameter is the start point of the range. This would be included in the range.
  • The “stop” parameter is the stop point of the range. This would be excluded from the range.
  • The “step” parameter indicates the steps to be added in a number to decide a random number.

Return a randomly-selected element from range(start, stop, step).
Difference between randrange([start[, stop, [step]) and range(start, stop, step) is that randrange([start[, stop, [step]) creates a range of integers. For instance, range(3, 10, 2) -> will include 3, 5, 7, 9. But randrange(3, 10, 2) will give ONE of the integers 3, 5, 7, 9.

# ---ON IDLE---   
>>> random.randrange(3,10,2) # Possible outcomes are one of 3,5,7,9
7

4.4.1 Syntax for writing / defining your function
To define a function, you have to follow a definite syntax ,as follows:

# ---ON JUPYTER---  
def func_name(arg1, arg2, ..., argN):
    statements (Optional)
    return some_object (Optional)

2. Flow of execution of a function
The first thing you need to understand is that every function has a function definition and a function call. Note the following:

  • A function definition is that piece of code, which creates the function, whereas a function call is that piece of code which uses the function.
  • A function must be defined before it can be used otherwise Python will not be able to recognize the name of the function.
  • This means that the def statement, which defines the function must come before the function call.

The following code explains the concept:

# ---ON IDLE---   
>>>def f1(a):
       x = a
       print('Argument passed-> ', x)

>>> f1(5)       # Function call with integer 5
Argument passed->5
>>> f1('cat')       # Function call with string ‘cat’
Argument passed->  cat
>>> f1([1,2,3,4])   # Function call with list [1, 2, 3, 4]
Argument passed->  [1, 2, 3, 4]

The following example clarifies the “flow of execution” where there is a function definition and a function call:-

In [2]:
def f1():
    print('Inside f1()')

print('Lets call f1()') #First line of executable code
f1()                    # Call to function f1()
print('Terminate')
Lets call f1()
Inside f1()
Terminate

4.3.7 Scope namespace and lifetime of variables
(iii) Variable inside a function definition (Example)

  • A variable defined inside a function definition, that is, code block beginning with the def statement is “local” to that function.
  • A variable in a script, which is outside any def call will be global to that program. This means that when you are inside a function call, the local variable and the global variable both will exist but when you are outside the function call, the local variable ceases to exist and only the global variables exist.
In [3]:
def f1(a):
    y = 'dog'# a and y are local variables
    print(y, a) # OK since y is defined here

x = 'cat'# x is a global variable. But y doesnt exist here
f1(x)
print(y) #Not OK. ERROR since y has gone "out of scope"
dog cat
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-3-c53f250c4a2b> in <module>()
      5 x = 'cat'# x is a global variable. But y doesnt exist here
      6 f1(x)
----> 7 print(y) #Not OK. ERROR since y has gone "out of scope"

NameError: name 'y' is not defined

(iv) Name clash in local and global scope
What does name clash mean? In Python you can assign different values to a variable name a number of times and the variable will point to the value by the latest assignment:

# ---ON IDLE---   
x = 'cat'
print('x is-> ', x) # x is cat
x = 'dog'
print('Now x is -> ', x) # x becomes dog

But if you assign different values to the SAME variable name in DIFFERENT scopes, then you have what is a potential name clash. How does Python interpreter resolve this? It follows a simple rule:

  • in local scope the local name will “generally prevail”. Note that the term “generally prevail” does not mean that the global variables cannot be accessed from local scope. There are ways to access the global variable in local scope also. This is covered later, and can be ignored for the present.)
  • while in global scope there is no problem because the local variable will not exist in global scope.

This is best understood by an example:-

In [4]:
x = 'cat'
def f3():
    x = 'dog'
    print('x inside the function call is-> ', x)
f3()
print('x in global scope is-> ', x)
x inside the function call is->  dog
x in global scope is->  cat

(v) Creating two local scopes in a global scope.
One can create a number of different local scopes within a global scope. This is best understood by the following example:

In [5]:
x = 'cat'
def f1():                   
    x = 'dog'
    print('x in f1-> ', x)  

def f2():
    x = 'rat'
    print('x in f2-> ', x)

f1()    # Prints x in scope of f1()
f2()    # Prints x in scope of f2()
print("x in global scope-> ",x)
x in f1->  dog
x in f2->  rat
x in global scope->  cat

(vi) Defining a function inside another function(Nested functions)
In Python it is permitted to define one function inside another function (Also called nested functions). But if you do so you cannot access the inner function from out of the enclosing function. In general, this is not a good programming practice. The following example shows the concept. Here, f1() is the enclosing, that is, outer function and f2() is the enclosed, that is, inner function.

In [6]:
x = 'cat'
def f1():
    x = 'dog'
    print('x in f1-> ', x)
    def f2():
        x = 'rat'
        print('x in f2-> ', x)
    f2()# You can call f2() here, since this is scope of f1()
f1()
# f2(x) is not callable from global scope
print('x in global scope-> ', x)
x in f1->  dog
x in f2->  rat
x in global scope->  cat

(vii) Order of search in nested functions
Note that nested functions create nested scope. In the above example, there was an enclosing function f1() and there was a nested function f2(). Both f1() and f2() defined x inside its scope. But suppose the nested function f2() did not define a value of x and it was asked to print x, what would it print? It would print the value of x in f1(). This is shown as follows:-

In [7]:
x = 'cat'
def f1(a):
    x = 'dog'
    print('x in f1-> ', x)
    def f2(a):
        #x = 'rat'
        print('x in f2-> ', x)
    f2(x) # This is scope of f1
f1(x) # Will print x of scope of f1
# f2(x) is not callable from global scope
print('x in global scope -> ', x)
x in f1->  dog
x in f2->  dog
x in global scope ->  cat